package org.muxiao.seckill.controller;

import cn.hutool.json.JSONUtil;
import com.ramostear.captcha.HappyCaptcha;
import com.ramostear.captcha.common.Fonts;
import com.ramostear.captcha.support.CaptchaStyle;
import com.ramostear.captcha.support.CaptchaType;
import org.muxiao.seckill.config.AccessLimit;
import org.muxiao.seckill.pojo.SecKillOrder;
import org.muxiao.seckill.pojo.SeckillMessage;
import org.muxiao.seckill.pojo.User;
import org.muxiao.seckill.rabbitmq.MQSenderMessage;
import org.muxiao.seckill.service.GoodsService;
import org.muxiao.seckill.service.OrderService;
import org.muxiao.seckill.vo.GoodsVo;
import org.muxiao.seckill.vo.RespBean;
import org.muxiao.seckill.vo.RespBeanEnum;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 秒杀 Controller
 */
@Controller
@RequestMapping("/seckill")
public class SecKillController implements InitializingBean {

    @Resource
    private GoodsService goodsService;

    @Resource
    private OrderService orderService;

    /**
     * redis 模板
     */
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * MQ 发送消息
     */
    @Resource
    private MQSenderMessage mqSenderMessage;

    /**
     * 使用自定义的 redis 脚本
     */
    @Resource
    private RedisScript<Long> script;

    /**
     * 定义map 记录秒杀商品是否还有库存，作为内存标记
     */
    private Map<Long, Boolean> entryStockMap = new HashMap<>();

    /**
     * 秒杀加入消息队列, 使用 rabbitMQ 实现秒杀的异步请求
     * 用 jmeter 进行压力测试时，请使用此接口
     *
     * @param model
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping("/doSeckill")
    public String doSecKill(Model model, User user, Long goodsId) {

        if (user == null) {
            return "login";
        }

        //对map进行判断[内存标记],如果商品在map已经标记为没有库存，则直接返回，无需进行Redis预减
        if (entryStockMap.get(goodsId)) {
            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }

        // 将 user 加入 model, 进行页面request域共享
        model.addAttribute("user", user);

        //判断库存
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        if (goodsVo.getStockCount() < 1) {

            model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
            return "secKillFail";
        }

        //判断用户是否复购
        // SecKillOrder secKillOrder = secKillOrderService.getOne(new QueryWrapper<SecKillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));
        SecKillOrder secKillOrder = (SecKillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (secKillOrder != null) {
            model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage());
            return "secKillFail";
        }

        //库存预减
        //Long decrement = redisTemplate.opsForValue().decrement("secKillGoods:" + goodsId);
        //if (decrement < 0) {
        //    entryStockMap.put(goodsId, true);
        //
        //    redisTemplate.opsForValue().increment("secKillGoods:" + goodsId);
        //    model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
        //    return "secKillFail";
        //}

        //---------使用 Redis 分布式锁--------
        //1. 对于当前项目而言，使用redisTemplate.opsForValue().decrement() 就可以控制抢购,因为该方法具有原子性和隔离性
        //2. 考虑到如果有比较多的操作，需要保证隔离性，也就是说，不是简单的-1,而是有多个操作
        //   这样就需要扩大隔离性的范围，部分操作还需要原子性, 我们给小伙伴演示一下Redis分布式锁的使用
        //3. 我们看看以前是如何使用Redis分布式锁的

        //获取锁，setnx
        //得到一个 uuid 值，作为锁的值
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);

        if (lock) {

            //进行库存预减
            Long decrement = redisTemplate.opsForValue().decrement("secKillGoods:" + goodsId);
            if (decrement < 0) {

                //当前秒杀的商品，已经没有库存，将该商品的库存标记为true，并释放锁
                entryStockMap.put(goodsId, true);
                //恢复库存为0
                redisTemplate.opsForValue().increment("secKillGoods:" + goodsId);

                //释放锁. 使用 redis+lua 脚本释放锁
                redisTemplate.execute(script, Arrays.asList("lock"), uuid);

                model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());
                return "secKillFail";
            }

            //释放分布式锁
            redisTemplate.execute(script, Arrays.asList("lock"), uuid);

        } else {
            //获取锁失败,返回个信息[本次抢购失败，请再次抢购...]
            model.addAttribute("errmsg", RespBeanEnum.SEC_KILL_RETRY.getMessage());
            return "secKillFail";
        }

        //抢购,向消息队列发送秒杀请求,实现了秒杀异步请求
        //这里我们发送秒杀消息后，立即快速返回结果[临时结果] - "比如排队中.."
        //客户端可以通过轮询，获取到最终结果
        //创建SeckillMessage
        SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
        String message = JSONUtil.toJsonStr(seckillMessage);
        mqSenderMessage.sendSecKillMessage(message);
        model.addAttribute("errmsg", "秒杀排队中");

        return "secKillFail";
    }


    /**
     * 最终版 - 隐藏秒杀接口
     * 加入秒杀安全：校验路径后，进行异步消息队列的请求
     * 使用此接口，需要到 goodsDetail.html 中将对应的注释打开
     */
    @RequestMapping("/{path}/doSeckill")
    @ResponseBody
    public RespBean doSecKill(@PathVariable String path, Model model, User user, Long goodsId) {

        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }

        //对map进行判断[内存标记],如果商品在map已经标记为没有库存，则直接返回，无需进行Redis预减
        if (entryStockMap.get(goodsId)) {
            return RespBean.error(RespBeanEnum.ENTRY_STOCK);
        }

        //校验路径
        boolean checkPath = orderService.checkPath(user, goodsId, path);
        if (!checkPath) {
            return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
        }

        //判断库存
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        if (goodsVo.getStockCount() < 1) {
            return RespBean.error(RespBeanEnum.ENTRY_STOCK);
        }

        //判断用户是否复购
        //SecKillOrder secKillOrder = secKillOrderService.getOne(new QueryWrapper<SecKillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));
        SecKillOrder secKillOrder = (SecKillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (secKillOrder != null) {
            return RespBean.error(RespBeanEnum.REPEAT_ERROR);
        }

        //库存预减
        Long decrement = redisTemplate.opsForValue().decrement("secKillGoods:" + goodsId);
        if (decrement < 0) {
            entryStockMap.put(goodsId, true);

            redisTemplate.opsForValue().increment("secKillGoods:" + goodsId);
            return RespBean.error(RespBeanEnum.ENTRY_STOCK);
        }

        //抢购,向消息队列发送秒杀请求,实现了秒杀异步请求
        //这里我们发送秒杀消息后，立即快速返回结果[临时结果] - "比如排队中.."
        //客户端可以通过轮询，获取到最终结果
        //创建SeckillMessage
        SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
        String message = JSONUtil.toJsonStr(seckillMessage);
        mqSenderMessage.sendSecKillMessage(message);

        return RespBean.error(RespBeanEnum.SEC_KILL_WAIT);
    }


    /**
     * 获取秒杀路径
     * 获取临时秒杀路径的同时校验验证码
     *
     * @param user
     * @param goodsId
     * @param captcha
     * @param request
     * @return
     */
    @RequestMapping("/path")
    @ResponseBody
    @AccessLimit(second = 5, maxCount = 5, needLogin = true)
    public RespBean getPath(User user, Long goodsId, String captcha, HttpServletRequest request) {
        if (user == null || goodsId < 0) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }

        // 增加一个业务逻辑,校验用户输入的验证码是否正确
        boolean checkCaptcha = orderService.checkCaptcha(user, goodsId, captcha);
        if (!checkCaptcha) {
            return RespBean.error(RespBeanEnum.CAPTCHA_ERROR);
        }

        String path = orderService.createPath(user, goodsId);
        return RespBean.success(path);
    }

    /**
     * 生成验证码-使用 Happy Captcha
     *
     * @param request
     * @param response
     * @param user
     * @param goodsId
     */
    @RequestMapping("/captcha")
    public void captcha(HttpServletRequest request, HttpServletResponse response, User user, Long goodsId) {

        if (user != null && goodsId > 0) {
            //生成验证码，并输出
            //注意，该验证码，默认就保存到session中, key是 happy-captcha
            HappyCaptcha.require(request, response).style(CaptchaStyle.IMG)            //设置展现样式为动画
                    .type(CaptchaType.NUMBER)            //设置验证码内容为字母
                    .length(5)                            //设置字符长度为6
                    .width(220)                            //设置动画宽度为220
                    .height(100)                            //设置动画高度为80
                    // .font(Fonts.getInstance().zhFont())    //设置汉字的字体
                    .font(Fonts.getInstance().defaultFont())    //设置汉字的字体
                    .build().finish();                //生成并输出验证码

            //把验证码的值，保存Redis [考虑项目分布式], 设置了验证码的失效时间100s
            //key: captcha:userId:goodsId
            String captcha = (String) request.getSession().getAttribute("happy-captcha");
            redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, captcha, 100, TimeUnit.SECONDS);

            //手动清理Session中存放的验证码，HappyCaptcha验证码的Key为“happy-captcha”
            HappyCaptcha.remove(request);
        }
    }


    /**
     * 该方法是在SeckillController类/对象的所有属性，都是初始化后，自动执行的
     * 这里我们就可以将所有秒杀商品的库存量，加载到Redis
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        //查询所有的秒杀商品
        List<GoodsVo> list = goodsService.findGoodsVo();
        //先判断是否为空
        if (CollectionUtils.isEmpty(list)) {
            return;
        }

        //遍历List,然后将秒杀商品的库存量，放入到Redis
        //秒杀商品库存量对应key : seckillGoods:商品id
        list.forEach(goodsVo -> {
            redisTemplate.opsForValue().set("secKillGoods:" + goodsVo.getId(), goodsVo.getStockCount());

            //初始化map, 内存标记, 记录秒杀商品是否还有库存
            //如果goodsId : false 表示有库存
            //如果goodsId : true 表示没有库存
            entryStockMap.put(goodsVo.getId(), false);
        });
    }
}
